package com.noahsloan.nutils.bytes;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Scanner;

import com.noahsloan.nutils.InToOut;
import com.noahsloan.nutils.streams.DelimitedInputStream;
import com.noahsloan.nutils.streams.PartialInputStream;


/**
 * Library of utility methods, primarily useful from converting primitive types
 * to/from bytes.
 * 
 * @author Noah Sloan
 * 
 */
public class Bytes {
    /**
     * Mask with the last 8 bits set.
     */
    public static final int BYTE_MASK = (~0) >>> (Integer.SIZE - Byte.SIZE);

    // OK, so maybe it's a little overboard to guard against the size of a byte
    // changing, but... it could. Someday.

    private static final int TWOMB = 2097152;

    /**
     * The number of bytes in an Integer.
     */
    public static final int INT_SIZE = Integer.SIZE / Byte.SIZE;

    /**
     * Th number of bytes in a Long.
     */
    public static final int LONG_SIZE = Long.SIZE / Byte.SIZE;

    /**
     * The number of bytes in a Float.
     */
    public static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE;

    /**
     * The number of bytes in Double.
     */
    public static final int DOUBLE_SIZE = Double.SIZE / Byte.SIZE;

    /**
     * Write <code>i</code> to <code>out</code> as {@link #INT_SIZE} bytes,
     * Most significant byte first. This is purely a convenience mechansim. For
     * other numeric types, you can do
     * <code>out.write(toByteArray(type...))</code>
     * 
     * @param out
     *            the stream to write to
     * @param i
     *            the int to write
     * @return out
     * @throws IOException
     *             if <code>out.write</code> throws and exception.
     */
    public static OutputStream encodeInt(OutputStream out, int i)
            throws IOException {
        out.write(intToByteArray(i));
        return out;
    }

    /**
     * Reads {@link #INT_SIZE} bytes from the stream and returns an integer.
     * 
     * @param in
     * @return
     * @throws IOException
     * @throws UnexpectedEndOfStreamException
     */
    public static int decodeInt(InputStream in) throws IOException,
            UnexpectedEndOfStreamException {
        byte[] bytes = fill(in, new byte[INT_SIZE]);
        return decodeInt(bytes);
    }

    /**
     * Equivalent to decodeInt(bytes,0)
     * 
     * @param bytes
     * @return an integer
     */
    public static int decodeInt(byte[] bytes) {
        return decodeInt(bytes, 0);
    }

    /**
     * Converts bytes to an integer.
     * 
     * @param bytes
     *            the bytes to decode
     * @param offset
     *            the index of the first byte
     * @return the bytes concatenated together
     * @throws ArrayIndexOutOfBoundsException
     *             if bytes is too short
     * @see #INT_SIZE
     */
    public static int decodeInt(byte[] bytes, int offset) {
        return (int) decode(bytes, offset, INT_SIZE);
    }

    /**
     * Same as decodeLong(bytes,0)
     * 
     * @param bytes
     * @return
     * @see #decodeLong(byte[], int)
     */
    public static long decodeLong(byte[] bytes) {
        return decodeLong(bytes, 0);
    }

    /**
     * Converts bytes into a long.
     * 
     * @param bytes
     *            the bytes to decode from
     * @param offset
     *            the index into bytes of the first byte of the long
     * @return a long value from the bytes
     */
    public static long decodeLong(byte[] bytes, int offset) {
        return decode(bytes, offset, LONG_SIZE);
    }

    /**
     * 
     * @param bytes
     * @return decodeFloat(bytes, 0)
     */
    public static float decodeFloat(byte[] bytes) {

        return decodeFloat(bytes, 0);
    }

    /**
     * Decodes a signed Java float from the given bytes.
     * 
     * @param bytes
     * @param offset
     * @return a float decoded from the given bytes.
     */
    public static float decodeFloat(byte[] bytes, int offset) {
        return Float.intBitsToFloat(decodeInt(bytes, offset));
    }

    /**
     * 
     * @param bytes
     * @return decodeDouble(bytes,0)
     */
    public static double decodeDouble(byte[] bytes) {
        return decodeDouble(bytes, 0);

    }

    /**
     * Returns a signed Java double.
     * 
     * @param bytes
     * @param offset
     * @return a double, decoded from the given bytes
     */
    public static double decodeDouble(byte[] bytes, int offset) {
        return Double.longBitsToDouble(decodeLong(bytes, offset));
    }

    /**
     * Internal decode method.
     * 
     * @param bytes
     * @param offset
     * @param size
     * @return
     */
    private static long decode(byte[] bytes, int offset, int size) {
        long value = 0;
        for (int i = offset; i < offset + size; i++) {
            value = value << Byte.SIZE;
            value += unsign(bytes[i]);
        }
        return value;
    }

    /**
     * Attempts to fill <code>bytes</code> from <code>in</code>.
     * 
     * @param in
     *            the stream to read bytes from.
     * @param bytes
     *            the array to fill.
     * @return <code>bytes</code> , filled.
     * @throws IOException
     *             if an I/O error occurs
     * @throws UnexpectedEndOfStreamException
     *             if the end of stream is reached before <code>bytes</code>
     *             can be filled. Whatever bytes were read will be available in
     *             <code>bytes</code> if the caller has retained a reference
     *             to it
     */
    public static byte[] fill(InputStream in, byte[] bytes) throws IOException,
            UnexpectedEndOfStreamException {
        int done = 0;
        while (done < bytes.length) {
            int expecting = bytes.length - done;
            int read = in.read(bytes, done, expecting);
            if (read == -1) {
                throw new UnexpectedEndOfStreamException(expecting);
            }
            done += read;
        }
        return bytes;
    }

    /**
     * Converts a single integer to a byte array.
     * 
     * @param i
     * @return
     */
    public static byte[] intToByteArray(int i) {
        int[] a = { i };
        return toByteArray(a);
    }

    /**
     * @param i
     *            the int(s) to convert
     * @return a byte array encoding i
     */
    public static byte[] toByteArray(int... i) {
        long[] l = new long[i.length];
        for (int j = 0; j < l.length; j++) {
            l[j] = i[j];
        }
        return toByteArray(Integer.SIZE / Byte.SIZE, l);
    }

    /**
     * 
     * @param l
     *            the long(s) to convert
     * @return a byte array encoding the long(s)
     */
    public static byte[] toByteArray(long... l) {
        return toByteArray(Long.SIZE / Byte.SIZE, l);
    }

    /**
     * 
     * @param f
     *            the float(s) to convert
     * @return a byte array encoding the float(s)
     */
    public static byte[] toByteArray(float... f) {
        int[] a = new int[f.length];
        for (int i = 0; i < a.length; i++) {
            a[i] = Float.floatToIntBits(f[i]);
        }
        return toByteArray(a);
    }

    /**
     * 
     * @param d
     *            the double(s) to convert
     * @return a byte array encoding the double(s)
     */
    public static byte[] toByteArray(double... d) {
        long[] a = new long[d.length];
        for (int i = 0; i < a.length; i++) {
            a[i] = Double.doubleToLongBits(d[i]);
        }
        return toByteArray(a);
    }

    /**
     * Internal conversion method. Encodes the last numBytes bytes of each long
     * into a byte array, starting with the highest order byte. i.e. a long of 4
     * bytes {127,34,45,-1} with numBytes = 3, then 34,45,-1 will be encoded, in
     * that order.
     * 
     * @param numBytes
     *            the number of bytes of each long to convert
     * @param l
     *            the value(s) to convert
     * @return a byte array encoding the value(s)
     */
    private static byte[] toByteArray(int numBytes, long... l) {
        byte[] bytes = new byte[l.length * numBytes];
        int index = 0;
        for (long num : l) {
            for (int i = 1; i <= numBytes; i++) {
                bytes[index] = (byte) (num >>> ((numBytes - i) * Byte.SIZE));
                index++;
            }
        }
        return bytes;
    }

    /**
     * Converts a byte into an unsigned integer.
     * 
     * @param b
     *            the byte to convert
     * @return an integer between 0 and 255 inclusive.
     */
    public static int unsign(byte b) {
        return BYTE_MASK & b;
    }

    /**
     * Reads the next sequence of characters not broken by whitespace -without-
     * buffering. This will consume all whitespace characters up to at least 1
     * non-whitespace character. In addition, 1 whitespace character following
     * the non-whitespace will be appended, unless none exists.
     * <p>
     * In other words, the very last character will be whitespace unless the end
     * of file has been reached.
     * <p>
     * 
     * @param in
     * @param encoding
     * @return the next token, or null if there are no non-whitespace characters
     *         remaining.
     * @throws IOException
     *             if an IOError occurs.
     * @throws UnsupportedEncodingException
     *             if the passed encoding is not supported.
     * @deprecated You should generally use {@link DelimitedInputStream} and/or
     *             {@link PartialInputStream} in combination with something like
     *             a {@link Scanner}.
     */
    @Deprecated
    public static String nextToken(InputStream in, String encoding)
            throws IOException {
        // we do assume that all the characters are the same number
        // of bytes
        byte[] next = new byte[" ".getBytes(encoding).length];
        ByteArrayOutputStream bout = new ByteArrayOutputStream();

        // eat the whitespace
        try {
            do {
                fill(in, next);
            } while (isWs(next, encoding));
        } catch (UnexpectedEndOfStreamException e) {
            return null;
        }

        // accumulate the non-whitespace
        bout.write(next);
        try {
            do {
                fill(in, next);
                bout.write(next);
            } while (!isWs(next, encoding));
        } catch (UnexpectedEndOfStreamException e) {
            // we have read some non-whitespace, so we return it.
        }

        return new String(bout.toByteArray(), encoding);
    }

    /**
     * 
     * @param chr
     * @param encoding
     * @return true if the given bytes constitute a whitespace character in the
     *         given encoding.
     * @throws UnsupportedEncodingException
     *             if the encoding is not supported, duh.
     * @deprecated this method is no longer necessary (and is not being
     *             maintained). See {@link DelimitedInputStream} and
     *             {@link PartialInputStream}
     */
    @Deprecated
    public static boolean isWs(byte[] chr, String encoding)
            throws UnsupportedEncodingException {
        byte[][] ws = { "\t".getBytes(encoding), " ".getBytes(encoding),
                "\r".getBytes(encoding), "\n".getBytes(encoding) };
        boolean is = false;
        for (byte[] b : ws) {
            is = is || Arrays.equals(b, chr);
        }
        return is;
    }

    /**
     * Equivalent to readTo(in, delimeter, Integer.MAX_VALUE).
     * 
     * @see #readTo(InputStream, byte[], int)
     */
    @Deprecated
    public static byte[] readTo(InputStream in, byte[] delimeter)
            throws IOException, DelimeterNotFoundException {
        return readTo(in, delimeter, Integer.MAX_VALUE);
    }

    /**
     * Reads bytes from in until the occurrence of delimeter or until limit
     * bytes have been read.
     * 
     * @param in
     *            the stream to read from
     * @param delimeter
     *            the bytes sequence to stop on
     * @param limit
     *            the maximum number of bytes to read
     * @return all the bytes up to and including the delimeter.
     * 
     * @throws DelimeterNotFoundException
     *             if the delimeter is not found before the EOS.
     * @throws IOException
     *             if an I/O error occurs
     * @deprecated use an {@link DelimitedInputStream} with a
     *             {@link PartialInputStream} instead.
     */
    @Deprecated
    public static byte[] readTo(InputStream in, byte[] delimeter, int limit)
            throws IOException, DelimeterNotFoundException {
        // establish a buffer of bytes that have been read
        int size = limit;
        if (size > TWOMB) {
            // if limit is greater than 2MB, only allocate 2MB for now
            size = TWOMB;
        }
        ByteArrayOutputStream bytes = new ByteArrayOutputStream(size);

        int len = delimeter.length;

        // create an array to hold the last len bytes read
        byte[] current = new byte[len];
        int start = 0;

        // read and save the 1st len bytes
        try {
            fill(in, current);
        } catch (UnexpectedEndOfStreamException e) {
            // wrap the bytes that were read
            byte[] read = new byte[len - e.getExpected()];
            System.arraycopy(current, 0, read, 0, read.length);
            throw new DelimeterNotFoundException(read);
        }
        bytes.write(current);

        // pull bytes from in until the last len bytes read match the
        // delimeter
        boolean done = false;
        while (!done && bytes.size() < size) {
            done = true;
            // check the current set
            for (int i = 0; i < len; i++) {
                if (current[(start + i) % len] != delimeter[i]) {
                    done = false;
                }
            }
            if (!done) {
                // read the next byte
                current[start] = (byte) in.read();
                if (current[start] != -1) {
                    // copy each byte to the read buffer
                    bytes.write(current[start]);
                    start = (start + 1) % len;
                } else {
                    throw new DelimeterNotFoundException(bytes.toByteArray());
                }
            }
        }

        return bytes.toByteArray();
    }

    /**
     * Takes bytesCount bytes from in and writes them to out.
     * 
     * @param in
     *            the byte source.
     * @param out
     *            the byte sink.
     * @param byteCount
     *            the number of bytes to transfer.
     * @throws IOException
     *             if an IOError occurs.
     * @deprecated Use {@link PartialInputStream} and {@link InToOut} instead.
     */
    @Deprecated
    public static void transfer(InputStream in, OutputStream out, long byteCount)
            throws IOException {
        long read = 0;
        byte[] buffer = new byte[1500]; // 1.5k buffer
        while (read < byteCount) {
            int max;
            if (byteCount - read > Integer.MAX_VALUE) {
                max = Integer.MAX_VALUE;
            } else {
                max = (int) (byteCount - read);
            }
            if (max > buffer.length) {
                max = buffer.length;
            }
            int len = in.read(buffer, 0, max);
            read += len;
            out.write(buffer, 0, len);
        }
    }
}
